package com.docjohnson.pamphlet.utils;

import java.util.Iterator;
import java.util.LinkedList;

public abstract class LruCache<K, V> {
	private LinkedList<CacheEntry> cache = new LinkedList<CacheEntry>();
	private int maxCapacity;
	
	public LruCache(int maxCapacity) {
		this.maxCapacity = maxCapacity;
	}
	
	/**
	 * Called after cache miss
	 * */
	public abstract V create(K key);
	
	
	/**
	 * Called when entry was removed from cache.
	 * 
	 * Override this method to do you operations with removed entry, e.g recycling bitmaps
	 * */
	public void entryRemoved(K key, V value) {
		
	}
	 
	
	/**
	 * Caches value for key. The value is moved to the head of the queue.
	 * 
	 * @return true if entry is added to cache false otherwise
	 * */
	public final boolean put(K key, V value) {
		int cacheCapacity = getCacheCapacity() + sizeOf(key, value);
		
		if (cacheCapacity <= maxCapacity) {
			cache.addFirst(new CacheEntry(key, value));
			return true;
		} else {
			// trying to remove last entry to add this one
			CacheEntry lastEntry = cache.getLast();
			
			int lastEntrySize = sizeOf(lastEntry.key, lastEntry.value);
			
			if (cacheCapacity - lastEntrySize <= maxCapacity) {
				cache.removeLast();
				entryRemoved(lastEntry.key, lastEntry.value);
				cache.addFirst(new CacheEntry(key, value));
				
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * This will return value for key.
	 *  
	 * */
	public final V get(K key) {
		int location = getIndexForKey(key);
		V value;
		if (location == -1) { // cache miss
			value = create(key);
		} else {
			value = cache.remove(location).value;
		}
		
		int cacheCapacity = getCacheCapacity() + sizeOf(key, value);
		
		if (cacheCapacity <= maxCapacity) {
			cache.addFirst(new CacheEntry(key, value));
		} else {
			// trying to remove last entry to add this one
			CacheEntry lastEntry = cache.getLast();
			
			int lastEntrySize = sizeOf(lastEntry.key, lastEntry.value);
			
			if (cacheCapacity - lastEntrySize <= maxCapacity) {
				cache.removeLast();
				entryRemoved(lastEntry.key, lastEntry.value);
				cache.addFirst(new CacheEntry(key, value));
			}
		}
		
		return value;
	}

	/**
	 * @return the size of entry for key and value
	 * 
	 * You must override this to specify your own size
	 * */
	protected int sizeOf(K key, V value) {
		return 1;
	}
	
	
	/**
	 * @return cache capacity
	 * */
	public final int size() {
		return getCacheCapacity();
	}
	
	
	/**
	 * Clears cache
	 * */
	public final void clear() {
		Iterator<CacheEntry> iterator = cache.iterator();
		while (iterator.hasNext()) {
			CacheEntry entry = iterator.next();
			entryRemoved(entry.key, entry.value);
		}
		
		cache.clear();
	}
	
	private int getCacheCapacity() {
		int size = 0;
		Iterator<CacheEntry> iterator = cache.iterator();
		while (iterator.hasNext()) {
			CacheEntry entry = iterator.next();
			
			size += sizeOf(entry.key, entry.value);
		}

		return size;
	}
	
	private int getIndexForKey(K key) {
		Iterator<CacheEntry> iterator = cache.iterator();
		int index = 0;
		while (iterator.hasNext()) {
			CacheEntry entry = iterator.next();
			if (entry.key.equals(key)) {
				return index; 
			}
			
			index ++;
		}
		
		return -1;
	}
	
	private class CacheEntry {
		K key;
		V value;
		
		public CacheEntry(K key, V value) {
			this.key = key;
			this.value = value;
		}
	}
}